/*
 Arduino 5 Button LCD Display Menu
 By Joe (grugly at sdf dot org)
 https://notabug.org/grugly/Arduino_5_Button_16x2_LCD_Menu
*/

// this needs to be here for some reason
#ifdef __cplusplus
extern "C" {
#endif

// Include LCD library
#include "LiquidCrystal.h"

// Pin assignment
#define LCD_PIN_1	8
#define LCD_PIN_2	9
#define LCD_PIN_3	4
#define LCD_PIN_4	5
#define LCD_PIN_5	6
#define LCD_PIN_6	7
#define BACKLIGHT_PIN	10
#define BUTTON_PIN	A0

// Button reference voltage
#define BUTTON_NULL	1023
#define BUTTON_SELECT	639
#define BUTTON_LEFT	408
#define BUTTON_DOWN	255
#define BUTTON_UP	98
#define BUTTON_RIGHT	0

// Init LCD
LiquidCrystal lcd(LCD_PIN_1, LCD_PIN_2, LCD_PIN_3,
		  LCD_PIN_4, LCD_PIN_5, LCD_PIN_6);

// Defaults
int backlight =0;	// Holds backlight status
int brightness = 128;	// Brightnes of backlight
int sleep_time = 5000;	// Time before screen turns off
int refresh_delay = 500;// Info display refresh delay
int cycle_delay = 500;	// Work cycle refresh delay

// Timers
unsigned long run_duration, wake_time, wake_duration,
				last_refresh, last_cycle = 0;

// Displayed text for each menu item (16 char max)
String menu_list[] = {
	"Brightness",
	"Standby Delay",
	"Uptime"
};

// Count and store number of menu items
int items = round((sizeof(menu_list) / sizeof(String) - 1));
int item = 0;		// Menu position
int menu_displayed = 0;	// Menu displayed if 1

// Custom chars for LCD to animate a chicken bouncing across the screen
byte chicken0[8] = { // High bounce
	B00000,
	B00000,
	B01100,
	B11100,
	B01111,
	B01110,
	B00100,
	B00000
};
byte chicken1[8] = { // Low bounce
	B00000,
	B00000,
	B00000,
	B01100,
	B11100,
	B01111,
	B01110,
	B00000
};
byte chicken2[8] = { // Squatting to lay
	B00000,
	B00000,
	B00000,
	B00000,
	B01100,
	B11100,
	B01111,
	B01110
};
byte chicken3[8] = { // Laying the egg
	B00000,
	B00000,
	B01000,
	B00001,
	B01100,
	B11100,
	B01111,
	B01110
};
byte egg[8] = { // The egg
	B00000,
	B00000,
	B00000,
	B00000,
	B00100,
	B01110,
	B01110,
	B00100
};

// Display statistics or, in this case, a chicken
// bouncing across the screen and laying an egg

int anichar = 0; // Make chicken bounce
int anipos = 15; // across the LCD
void displayInfo() {
	if ((run_duration - last_refresh) >= refresh_delay) {

		// Animate a chicken to simulate actual data being displayed
		if (anipos >= 0) {
			if (anipos == 8) { // Lay egg at char 8
				lcd.setCursor(anipos, 0);
				lcd.write(2);
				lcd.print(" ");
				// Calls to delay() will block doWork() for
				// their duration so don't use them if the
				// work is critical
				delay(refresh_delay);
				lcd.setCursor(anipos, 0);
				lcd.write(3);
				delay(refresh_delay);
				lcd.setCursor((anipos - 1), 0);
				lcd.write(anichar);
				lcd.write(4);
				anipos -= 2;
			} else { // Continue bouncing chicken
				lcd.setCursor(anipos, 0);
				lcd.write(anichar);
				lcd.print(" ");
				anipos--;
			}
		} else { // Clear the last chicken and the egg and start again
			lcd.setCursor(0, 0);
			lcd.print(" ");
			lcd.setCursor(8, 0);
			lcd.print(" ");
			anipos = 15;
		}
		// Bounce chicken
		if (anichar == 0) { anichar++; }
		else { anichar--; }

		// Reset timer
		last_refresh = millis();
	}
}

// Read sensors, flick switches, etc..
void doWork() {
	// Flash a dot in the bottom corner of the LCD to simulate work
	if ((run_duration - last_cycle) >= cycle_delay) {
		lcd.setCursor(15, 1);
		lcd.print(".");
		last_cycle = millis();
	} else if ((run_duration - last_cycle) >= (cycle_delay / 2)) {
		lcd.setCursor(15, 1);
		lcd.print(" ");
	}
}

// Read a button press and return number of button pressed
int readButton() {
	int button_voltage = analogRead(BUTTON_PIN);
	int button_pressed = 0;

	if (button_voltage <= (BUTTON_NULL + 10) 
	 && button_voltage >= (BUTTON_NULL - 10)) {
		button_pressed = 0;
	}
	if (button_voltage <= (BUTTON_SELECT + 10)
	 && button_voltage >= (BUTTON_SELECT - 10)) {
		button_pressed = 1;
	}
	if (button_voltage <= (BUTTON_LEFT + 10)
	 && button_voltage >= (BUTTON_LEFT - 10)) {
		button_pressed = 2;
	}
	if (button_voltage <= (BUTTON_DOWN + 10)
	 && button_voltage >= (BUTTON_DOWN - 10)) {
		button_pressed = 3;
	}
	if (button_voltage <= (BUTTON_UP + 10)
	 && button_voltage >= (BUTTON_UP - 10)) {
		button_pressed = 4;
	}
	if (button_voltage <= (BUTTON_RIGHT + 10)
	 && button_voltage >= (BUTTON_RIGHT - 10)) {
		button_pressed = 5;
	}

	// Wait until button is released before we return
	while (button_voltage <= (BUTTON_NULL - 10)) {
		button_voltage = analogRead(BUTTON_PIN);
		delay(100);
	}

	if (button_pressed > 0) {
		wake_time = millis();	// Reset the backlight timer
	}

	return button_pressed;
}

// Display the menu item on the LCD
void displayItem(int item) {
	// Clear the screen and print the item title
	lcd.clear();
	lcd.print(String(menu_list[item]));

	// Display the current value in a readable form
	// Compiler says these need defining outside the case statement!
	int percent, sec;
	unsigned long d, h, m, s;
	switch (item) {
		case 0: // Brightness is displayed as a percentage
			// We don't want to turn the screen completely off
			// and a value of 256 will overflow it so we trim
			// the values. I like the result.
			percent = round(((brightness - 16) / 224.0) * 100);
			lcd.setCursor(0, 1);
			lcd.print(String(percent) + "%");
			break;
		case 1: // Stand-by delay is shown in seconds
			if (sleep_time < 11000) {
				sec = sleep_time / 1000;
				lcd.setCursor(0, 1);
				lcd.print(String(sec) + "sec");
			}
			else {
				lcd.setCursor(0, 1);
				lcd.print("OFF");
			}
			break;
		case 2: // Display uptime
			s = (run_duration / 1000) % 60;
			m = (run_duration / 60000) % 60;
			h = (run_duration / 3600000) % 24;
			d = (run_duration / 86400000) % 12;
			lcd.setCursor(0, 1);
			lcd.print(String(d) + "d " + String(h) + "h " + String(m) + "m " + String(s) + "s ");
			break;
	}
}

// Change values of menu items
// Action is 0 for decreasing (left button) and
// 1 for increasing (right button)
void updateItem(int item, int action) {
	switch (item) {
		case 0: // Brightness
			// don't dim the screen to off
			if (action == 0 && brightness != 16) {
				brightness = brightness - 16;
				analogWrite(BACKLIGHT_PIN, brightness);
			}
			// 256 overflows and the screen is quite bright anyway
			else if (action == 1 && brightness != 240) {
				brightness = brightness + 16;
				analogWrite(BACKLIGHT_PIN, brightness);
			}
			break;
		case 1: // Standby Delay
			// Minimum delay is 2 seconds
			if (action == 0 && sleep_time > 2000) {
				sleep_time -= 1000;
			}
			// Max is 10 seconds (11 is off)
			else if (action == 1 && sleep_time < 11000) {
				sleep_time += 1000;
			}
			break;
		// To display info, we don't need to modify the value
		// so for item[2] (Uptime) we don't take any action.
	}
	displayItem(item);
}

void setup(void) {
	// Setup LCD
	lcd.begin(16, 2);
	lcd.noCursor();
	lcd.createChar(0, chicken0);	// Create jumping chicken char
	lcd.createChar(1, chicken1);	// Create landing chicken char
	lcd.createChar(2, chicken2);	// Create squatting chicken char
	lcd.createChar(3, chicken3);	// Create laying chicken char
	lcd.createChar(4, egg);		// Create egg char

	// Setup button and backlight pins
	pinMode(BUTTON_PIN, INPUT);
	pinMode(BACKLIGHT_PIN, OUTPUT);

	// Turn backlight on
	analogWrite(BACKLIGHT_PIN, brightness);
	backlight = 1;

	return;
}

void loop(void) {
	// Note how long we have been running for
	run_duration = millis();
	// and how long since the last button push
	wake_duration = run_duration - wake_time;

	// Hide the menu/go to sleep
	if (backlight == 1 && wake_duration >= sleep_time) {
		if (menu_displayed == 1) {
			menu_displayed = 0;
			item = 0;
			lcd.clear();
			wake_time = millis();
		} else if (sleep_time != 11000) {
			digitalWrite(BACKLIGHT_PIN, LOW);
			lcd.clear();
			backlight = 0;
			wake_time = 0;
		}
	}

	// Read a button press
	int button = readButton();

	// Wake up when the select button is pushed
	if (button == 1 && backlight == 0) {
		analogWrite(BACKLIGHT_PIN, brightness);
		backlight = 1;
	}

	// Show the menu while awake and the select button is pushed
	else if (button == 1 && menu_displayed == 0) {
		// Set menu flag
		menu_displayed = 1;

		// Display the first item
		displayItem(item);
	}

	else if (button >= 1 && menu_displayed == 1) {
		switch (button) {
			case 1:	// Select button pushed
				// Exit menu
				menu_displayed = 0;
				item = 0;
				lcd.clear();
				return;
				break;
			case 2:	// Left button pushed
				// Decrease value
				updateItem(item, 0);
				break;
			case 3:	// Down button pushed
				// Display next menu item
				if (++item > items) { item = 0; }
				displayItem(item);
				break;
			case 4:	// Up button pushed
				// Display previous menu item
				if (--item < 0) { item = items; }
				displayItem(item);
				break;
			case 5:	// Right button pushed
				// Increase value
				updateItem(item, 1);
				break;
		}
	}

	// Show info while awake
	else if (backlight == 1 && menu_displayed == 0) {
		displayInfo();
	}

	// Always do something usefull..
	doWork();

	return;
}

#ifdef __cplusplus
}
#endif
